package be.dch.guice.context;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;

import javax.naming.Context;
import javax.naming.InitialContext;

import org.aopalliance.intercept.MethodInterceptor;
import org.apache.log4j.Logger;

import be.dch.guice.GuiceConfigReader;
import be.dch.guice.exceptions.GuiceClassPathXmlException;
import be.dch.guice.extra.SingletonInternal;
import be.dch.guice.extra.TrueSingletonProvider;
import be.dch.guice.modules.ExtraConfig;
import be.dch.guice.xsd.BindingType;
import be.dch.guice.xsd.ConstantBindingType;
import be.dch.guice.xsd.GuiceConfiguration;
import be.dch.guice.xsd.InterceptorBindingType;
import be.dch.guice.xsd.PropertiesBundleBindingType;
import be.dch.guice.xsd.PropertyType;
import be.dch.guice.xsd.ProviderBindingType;
import be.dch.guice.xsd.ScopeType;
import be.dch.guice.xsd.StaticInjectionType;
import be.dch.guice.xsd.TrueSingletonBindingType;
import be.dch.util.StringUtils;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.TypeLiteral;
import com.google.inject.matcher.Matcher;
import com.google.inject.matcher.Matchers;
import com.google.inject.name.Names;

@SuppressWarnings({ "rawtypes", "unchecked" })
public class ClassPathXmlContext extends AbstractModule {
    private static final Logger LOG = Logger.getLogger(ClassPathXmlContext.class);
    private final List<ExtraConfig> extra = new ArrayList<ExtraConfig>();
    private final String[] configFiles;

    public ClassPathXmlContext(final String[] configFileNames) {
	this.configFiles = configFileNames;
    }

    private List<GuiceConfiguration> readFile(final String configFile, final HashSet<String> processed) throws GuiceClassPathXmlException {
	ClassPathXmlContext.LOG.debug("reading Guice config " + configFile);
	processed.add(configFile);

	final List<GuiceConfiguration> configs = new ArrayList<GuiceConfiguration>();
	final GuiceConfiguration config = GuiceConfigReader.readConfig(configFile);

	configs.add(config);

	if ((config.getModuleDependencies() != null) && (config.getModuleDependencies().getModuleXml() != null)) {
	    for (final String includedConfig : config.getModuleDependencies().getModuleXml()) {
		String use = includedConfig;
		if (!use.startsWith("./")) {
		    use = "./" + use;
		}

		if (!processed.contains(use)) {
		    ClassPathXmlContext.LOG.debug("Configuring Guice - included config " + use);
		    final List<GuiceConfiguration> incConfigs = this.readFile(use.trim(), processed);
		    configs.addAll(incConfigs);
		} else {
		    ClassPathXmlContext.LOG.debug(use + " already bound, will not bind again");
		}
	    }

	}

	return configs;
    }

    @Override
    protected void configure() {
	for (final String configFile : this.configFiles) {
	    ClassPathXmlContext.LOG.debug("Binding Guice with " + configFile);
	    final List<GuiceConfiguration> allConfigs = this.readFile(configFile, new HashSet<String>());

	    final GuiceConfiguration topConfig = allConfigs.get(0);

	    ClassPathXmlContext.LOG.debug("Binding Guice with " + Integer.valueOf(allConfigs.size()) + " config files");

	    this.bind(Context.class).to(InitialContext.class);

	    for (final GuiceConfiguration config : allConfigs) {
		final String detailMsg = (config != topConfig) ? ".[" + config.getConfigName() + "]" : "";
		ClassPathXmlContext.LOG.info(super.getClass() + "Binding guice config [" + topConfig.getConfigName() + "]" + detailMsg);
		ClassPathXmlContext.LOG.debug(config.getConfigName() + "## configuring all standard bindings");

		for (final BindingType binding : config.getBinding()) {
		    this.processBinding(config, binding);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + "## configuring all true singleton bindings");
		for (final TrueSingletonBindingType binding : config.getTrueSingletonBinding()) {
		    this.processTrueSingletonBinding(config, binding);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + "## configuring all provider bindings");

		for (final ProviderBindingType binding : config.getProviderBinding()) {
		    this.processProviderBinding(config, binding);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + "## configuring all constant bindings");

		for (final ConstantBindingType constant : config.getConstantBinding()) {
		    this.processConstantBinding(config, constant);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + "## configuring all property bundle bindings");

		for (final PropertiesBundleBindingType bundle : config.getPropertiesBundleBinding()) {
		    this.processPropertiesBundle(bundle);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + " ## configuring all static injection requests");

		for (final StaticInjectionType injection : config.getStaticInjection()) {
		    this.processStaticInjection(config, injection);
		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + " ## configuring all interceptor bindings");

		for (final InterceptorBindingType interceptor : config.getInterceptorBinding()) {
		    this.processInterceptorBinding(config, interceptor);

		}

		ClassPathXmlContext.LOG.debug(config.getConfigName() + " ## configuring all module class dependencies");
		if (config.getModuleDependencies() != null) {
		    this.processModuleDependencies(config);
		}

		for (final ExtraConfig conf : this.extra) {
		    ClassPathXmlContext.LOG.debug(config.getConfigName() + " ## loading extra config module with name: " + conf.getClass().getSimpleName());
		    conf.process(config, this);
		}
	    }

	    ClassPathXmlContext.LOG.debug("extendConfigure of " + configFile + " completed...");
	}
    }

    private void processInterceptorBinding(final GuiceConfiguration config, final InterceptorBindingType interceptor) {
	try {
	    final Class impl = Class.forName(interceptor.getInterceptor());
	    final MethodInterceptor inc = (MethodInterceptor) impl.newInstance();
	    final Class annot = Class.forName(interceptor.getAnnotatedWith());
	    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interceptor: " + impl + " to annotation: " + annot);
	    if (StringUtils.isEmpty(interceptor.getMatcher())) {
		this.bindInterceptor(Matchers.any(), Matchers.annotatedWith(annot), new MethodInterceptor[] { inc });
	    } else {
		final Class match = Class.forName(interceptor.getMatcher());
		final Matcher matcher = (Matcher) match.newInstance();
		this.bindInterceptor(matcher, Matchers.annotatedWith(annot), new MethodInterceptor[] { inc });
	    }
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Interceptor Config in BaseExternalModule while reading : " + config.getConfigName(), e);

	} catch (final IllegalAccessException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Interceptor Config in BaseExternalModule while reading : " + config.getConfigName(), e);

	} catch (final InstantiationException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Interceptor Config in BaseExternalModule while reading : " + config.getConfigName(), e);

	}

    }

    private void processBinding(final GuiceConfiguration config, final BindingType binding) {
	try {
	    final Class iface = Class.forName(binding.getInterface());
	    final Class impl = Class.forName(binding.getImplementation());
	    final ScopeType scope = binding.getScope();

	    if (binding.getAnnotatedWith() == null) {
		ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interface: " + iface + " to impl: " + impl + " scope?: " + scope);
		if (ScopeType.SINGLETON.equals(scope)) {
		    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# binding singleton " + iface + " to impl: " + impl);
		    this.bind(iface).to(impl).in(Scopes.SINGLETON);
		} else {
		    this.bind(iface).to(impl);
		}
	    } else {
		final Class annot = Class.forName(binding.getAnnotatedWith());
		ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interface: " + iface + " to impl: " + impl + " annotated with: " + annot + " scope?: " + scope);
		if (ScopeType.SINGLETON.equals(scope)) {
		    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# binding singleton " + iface + " to impl: " + impl + " annotated with " + annot);
		    this.bind(iface).annotatedWith(annot).to(impl).in(Scopes.SINGLETON);
		} else {
		    this.bind(iface).annotatedWith(annot).to(impl);
		}
	    }
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Standard Binding Config in BaseExternalModule while reading : " + config.getConfigName(), e);
	}
    }

    private void processStaticInjection(final GuiceConfiguration config, final StaticInjectionType injection) {
	try {
	    final Class staticClass = Class.forName(injection.getStaticClass());

	    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Requesting static injection for " + staticClass);

	    this.requestStaticInjection(new Class[] { staticClass });
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Static Injection Config in BaseExternalModule while reading :" + config.getConfigName() + " item is: " + injection.getStaticClass(), e);
	}
    }

    private void processPropertiesBundle(final PropertiesBundleBindingType bundle) {
	ClassPathXmlContext.LOG.debug("Binding properties bundle with name " + bundle.getBundleName());

	final Map properties = new HashMap();
	for (final PropertyType prop : bundle.getProperty()) {
	    properties.put(prop.getName(), prop.getValue());
	}

	this.bind(new TypeLiteral() {
	}).annotatedWith(Names.named(bundle.getBundleName())).toInstance(Collections.unmodifiableMap(properties));
    }

    private void processTrueSingletonBinding(final GuiceConfiguration config, final TrueSingletonBindingType binding) {
	try {
	    final Class iface = Class.forName(binding.getInterface());
	    final Class impl = Class.forName(binding.getImplementation());

	    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interface: " + iface + " as true singleton to implementation " + impl);

	    this.bind(iface).annotatedWith(SingletonInternal.class).to(impl);
	    this.bind(iface).toProvider(new TrueSingletonProvider(iface));
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from True Singleton Binding Config in BaseExternalModule while reading : " + config.getConfigName(), e);

	}
    }

    private void processConstantBinding(final GuiceConfiguration config, final ConstantBindingType constant) {
	final String constantName = constant.getConstantName();
	final String constantValue = constant.getConstantValue();

	ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding constant " + constantName + " to value '" + constantValue + "'");

	this.bindConstant().annotatedWith(Names.named(constantName)).to(constantValue);
    }

    private void processProviderBinding(final GuiceConfiguration config, final ProviderBindingType binding) {
	try {
	    final Class iface = Class.forName(binding.getInterface());
	    final Class provider = Class.forName(binding.getProvider());
	    final ScopeType scope = binding.getScope();

	    if (binding.getAnnotatedWith() == null) {
		ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interface: " + iface + " to provider: " + provider + " scope?: " + scope);

		if (ScopeType.SINGLETON.equals(scope)) {
		    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# binding singleton " + iface + " to impl: " + provider);

		    this.bind(iface).toProvider(provider).in(Scopes.SINGLETON);
		} else {
		    this.bind(iface).toProvider(provider);
		}
	    } else {
		final Class annot = Class.forName(binding.getAnnotatedWith());
		ClassPathXmlContext.LOG.debug(config.getConfigName() + "# Binding interface: " + iface + " to provider: " + provider + " annotated with: " + annot + " scope?: " + scope);

		if (ScopeType.SINGLETON.equals(scope)) {
		    ClassPathXmlContext.LOG.debug(config.getConfigName() + "# binding singleton " + iface + " to impl: " + provider + " annotated with " + annot);

		    this.bind(iface).annotatedWith(annot).toProvider(provider).in(Scopes.SINGLETON);
		} else {
		    this.bind(iface).annotatedWith(annot).toProvider(provider);
		}
	    }
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error("Exception from Provider Binding Config in BaseExternalModule while reading: " + config.getConfigName(), e);
	}
    }

    private void processModuleDependencies(final GuiceConfiguration config) {
	// TODO add logging
	try {
	    final List<String> clazzList = config.getModuleDependencies().getModuleClass();
	    for (final String clazzName : clazzList) {
		final Class clazz = Class.forName(clazzName);
		clazz.newInstance();
	    }
	} catch (final ClassNotFoundException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error(e);
	} catch (final InstantiationException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error(e);
	} catch (final IllegalAccessException e) {
	    this.addError(e);
	    ClassPathXmlContext.LOG.error(e);
	}
    }

    public void addExtraConfig(final ExtraConfig config) {
	this.extra.add(config);
    }

}
